﻿using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.VisualStyles;

namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor
{
    public enum 定位模式
    {
        外框模式,
        四角定位,
    }

    internal class ReadAnswerSheet
    {
        /// <summary>
        /// 匹配答案
        /// </summary>
        /// <param name="imgPath">图像文件路径。</param>
        /// <param name="ad">答题卡定义。</param>
        /// <param name="numbersList">返回考试证号各位组成的列表。如果某位少于一个字符或多于一个字符，则为错误。</param>
        /// <param name="answersList">返回识别出来的答案字串组成的列表。</param>
        /// <param name="尝试次数">阈值调整次数。</param>
        /// <param name="精确匹配模式">是否启用精确匹配模式（按轮廓匹配）。</param>
        /// <param name="par1">[缺省为 150]阈值Min可以默认给150。</param>
        /// <param name="par2">[缺省为 255]阈值Max。</param>
        /// <returns></returns>
        public static Mat 匹配答案(string imgPath, 答题卡定义 ad,
                                    ref List<string> numbersList,           // 考试证号列表
                                    ref List<string> answersList,       // 答案列表
                                    out int 尝试次数,
                                    bool 精确匹配模式,                  // 是否精确匹配
                                    bool 是否带批改信息,
                                    定位模式 locateMode,
                                    ref Mat srcMat,
                                    int? par1 = null, int? par2 = null
            )
        {
            修整参数值(ref par1, 150);
            修整参数值(ref par2, 255);

            Mat 答题卡图 = Cv2.ImRead(imgPath);                           // 载入图像

            //double rateX = (double)500 / 答题卡图.Width;  // 缩小会导致找不到边框
            //var newWidth = 答题卡图.Width * rateX;
            //var newHeight = 答题卡图.Height * rateX;

            //Mat 小答题卡图 = new Mat();
            //Cv2.Resize(答题卡图, 小答题卡图, new Size(newWidth, newHeight));

            //灰度转化
            Mat 灰度图 = new Mat();
            Cv2.CvtColor(答题卡图, 灰度图, ColorConversionCodes.RGB2GRAY);
#if DEBUG
            显示图像(灰度图, "灰度图");
#endif
            //进行高斯滤波
            Mat 高斯图 = new Mat();
            Cv2.GaussianBlur(灰度图, 高斯图, new Size(15, 15), 3);
#if DEBUG
            显示图像(高斯图, "高斯图");
#endif

            Point[] 轮廓数组;
            Mat canny = new Mat();
            if (ad.读卡时尝试连接外框缺口)
            {
                // 性能很低，只有单个重试时才应该支持

                // 步骤2：自适应阈值（生成修复后的二值图）
                Mat binary = new Mat();
                Cv2.AdaptiveThreshold(高斯图, binary, 255,
                    AdaptiveThresholdTypes.MeanC, ThresholdTypes.Binary, 15, 3);

                // 步骤3：形态学闭合（连接小缺口）
                Mat kernelx = Cv2.GetStructuringElement(MorphShapes.Cross, new Size(5, 5)); // 十字核
                Cv2.Dilate(binary, binary, kernelx); // 膨胀
                Cv2.Erode(binary, binary, kernelx);  // 腐蚀

                轮廓数组 = 寻找边界(binary, locateMode, ref canny);               // 寻找边界（以最大的矩形为外框）
            }
            else
            {
                轮廓数组 = 寻找边界(高斯图, locateMode, ref canny);               // 寻找边界（以最大的矩形为外框）
            }

            // 这里会调用：显示图像(canny, "canny");

            Mat 初始鸟瞰图 = 封装成鸟瞰图(高斯图, 轮廓数组);      // 封装成鸟瞰图
            var _left = 初始鸟瞰图.Width * ad.边框宽度比;
            var _top = 初始鸟瞰图.Height * ad.边框高度比;
            var _width = 初始鸟瞰图.Width - _left * 2;
            var _height = 初始鸟瞰图.Height - _top * 2;
            var 切边矩形 = new OpenCvSharp.Rect((int)_left, (int)_top, (int)_width, (int)_height);
            Mat 鸟瞰图 = new Mat(初始鸟瞰图, 切边矩形);
            srcMat = 鸟瞰图;
#if DEBUG
            显示图像(鸟瞰图, "鸟瞰");
#endif
            // 1. OTSU 阈值分割
            Mat 阈值图 = new Mat();
            int 选中选项数量 = 0;
            List<Point[]> 选择的轮廓列表 = new List<Point[]>();
            尝试次数 = 0;

            // 2. 自动调整阈值，取到填涂框才继续
            while (选中选项数量 <= 1)
            {
                if (尝试次数 >= 100)
                    return null;
                if (尝试次数 > 0)
                    par1--;
                尝试次数++;
                Cv2.Threshold(鸟瞰图, 阈值图, par1.Value, par2.Value, ThresholdTypes.BinaryInv);
                // 修改thresh或maxval可以调整轮廓取值范围(调的不好会直接取外面的大轮廓)

                // 形态学操作，去除小噪点
                Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
                Cv2.MorphologyEx(阈值图, 阈值图, MorphTypes.Close, kernel);

                var 填涂块宽度 = (double)(阈值图.Width / ad.答题卡列数.Value) / 3;    // 这里不能移到循环外面，否则 阈值图 的宽、高可能总是0！
                var 填涂块高度 = (double)(阈值图.Height / ad.答题卡行数.Value) / 3;   // 仅答题卡图测试用 1/2 较好；但打印带大段文字的答题纸后，发现 1/3 比较合适。

                选择的轮廓列表 = 选取选项轮廓位置(阈值图, 填涂块高度, 填涂块宽度);
                选中选项数量 = 选择的轮廓列表.Count();
            }

            // 3. 验证结果
            Mat 阈值图副本 = 阈值图.Clone();
            Cv2.CvtColor(阈值图副本, 阈值图副本, ColorConversionCodes.GRAY2BGR);
            // Cv2.DrawContours(阈值图副本, 选择的轮廓列表, -1, new Scalar(0, 0, 255), 3);
#if DEBUG
            显示图像(阈值图副本, "选项");
#endif
            // 4. 判断所有轮廓是否能与答题卡定义中的某个点对上

#if DEBUG
            Console.Write(numbersList);
            Console.Write(answersList);
#endif
            return 按答题卡定义取结果数组(阈值图副本, 选择的轮廓列表, ad, ref numbersList, ref answersList, 精确匹配模式, 是否带批改信息);
        }


        /// <summary>
        /// 如果没有提供值，就返回默认值。
        /// </summary>
        /// <param name="value">提供的值。</param>
        /// <param name="defaultValue">未提供参数值时返回的默认值。</param>
        public static void 修整参数值(ref int? value, int defaultValue = 0)
        {
            if (value == null)
                value = defaultValue;
        }


        /// <summary>
        /// 寻找边界
        /// </summary>
        /// <param name="mat"></param>
        /// <returns></returns>
        public static Point[] 寻找边界(Mat mat, 定位模式 locateMode, ref Mat canny, string size = "45,90,3,true")
        {
            //进行canny边缘检测
            //Cv2.Canny(高斯图, canny, 0, 180);
            string[] str = size.Split(',');
            Cv2.Canny(mat, canny, int.Parse(str[0]), int.Parse(str[1]), int.Parse(str[2]), bool.Parse(str[3]));
#if DEBUG
            显示图像(canny, "canny", 2);
#endif
            //寻找矩形边界
            Point[] result_contour;

            Point[][] contours;
            HierarchyIndex[] hierarchly;
            Cv2.FindContours(canny, out contours, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxSimple);

            if (locateMode == 定位模式.外框模式)
            {
                //Cv2.FindContours(canny, out 轮廓列表, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxNone);
                //Cv2.FindContours(canny, out 轮廓列表, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxTC89KCOS);
                //Cv2.FindContours(canny, out 轮廓列表, out hierarchly, RetrievalModes.External, ContourApproximationModes.ApproxTC89L1);

                if (contours.Length == 1)
                {
                    result_contour = contours[0];
                }
                else
                {
                    double maxArea = 0;
                    int largestContourIndex = -1;
                    Rect maxRect = Cv2.BoundingRect(contours[0]);
                    for (int i = 0; i < contours.Length; i++)
                    {
                        var contour = contours[i];
                        // canny.DrawContours(contours, i, Scalar.Red, 2);   // 轮廓多，性能极低
                        double area = Cv2.ContourArea(contour);
                        if (area > maxArea)
                        {
                            maxArea = area;
                            largestContourIndex = i;
                            maxRect = Cv2.BoundingRect(contour);
                        }
                    }

                    //largestContourIndex 不可能为 -1;
                    //取面积最大的矩形为检测填涂区域，所以在做答题卡时需要把填涂区的框做最大，或者自己调整，但上面的判断也需要一并调整
                    canny.Rectangle(maxRect, Scalar.White, 10);
#if DEBUG
                显示图像(canny, "xxx", 2);
#endif
                    result_contour = contours[largestContourIndex];
                }
            }
            else
            {
                // 存储所有矩形的顶点
                List<Point2f> allVertices = new List<Point2f>();

                // 遍历每个找到的轮廓
                foreach (var contour in contours)
                {
                    // 近似轮廓为多边形，精度为轮廓周长的2%
                    var approxCurve = Cv2.ApproxPolyDP(contour, Cv2.ArcLength(contour, true) * 0.02, true);

                    // 检查是否为四边形（矩形），并且是凸的
                    if (approxCurve.Length == 4 && Cv2.IsContourConvex(approxCurve))
                    {
                        // 获取矩形的顶点并将它们添加到allVertices列表中
                        Point2f[] vertices = Array.ConvertAll(approxCurve, point => new Point2f(point.X, point.Y)); allVertices.AddRange(vertices);
                    }
                }

                // 计算包含所有矩形的最小外接矩形
                RotatedRect minAreaRect = Cv2.MinAreaRect(allVertices.ToArray());

                // 获取最小外接矩形的顶点
                Point2f[] rectVertices = minAreaRect.Points();

                // 将顶点转换为整数坐标以便在图像上绘制
                result_contour = Array.ConvertAll(rectVertices, point => new Point((int)point.X, (int)point.Y));
            }

            return result_contour;
        }


        private static int imgShowIndex = 1;
        private static string showIndex = "";//显示步骤 1234

        public static void 显示图像(Mat mat, string name = "", int div = 5)
        {
            bool show = true;
            foreach (var item in showIndex)
            {
                int i = int.Parse(item.ToString());
                int i2 = (int)item;
                if (imgShowIndex == int.Parse(item.ToString()))
                {
                    show = true;
                }
            }

            if (!show)
            {
                imgShowIndex++;
                return;
            }

            var matmini = new Mat();
            Cv2.Resize(mat, matmini, new Size(mat.Width / div, mat.Height / div));

            Cv2.ImShow(!string.IsNullOrEmpty(name) ? $"{imgShowIndex} {name}" : imgShowIndex.ToString(), matmini);
            imgShowIndex++;
        }



        /// <summary>
        /// 对图像进行矫正(转为鸟瞰图，删除多余边界)
        /// </summary>
        public static Mat 封装成鸟瞰图(Mat mat, Point[] result_contour)
        {
            //使用DP算法拟合答题卡的几何轮廓,保存点集pts并顺时针排序
            double result_length = Cv2.ArcLength(result_contour, true);
            Point[] pts = Cv2.ApproxPolyDP(result_contour, result_length * 0.02, true);
            int width = 0;
            int height = 0;
            if (pts.Length == 4)
            {
                if (pts[1].X < pts[3].X)
                {
                    //说明当前为逆时针存储，改为顺时针存储（交换第2、4点）
                    Point p = new Point();
                    p = pts[1];
                    pts[1] = pts[3];
                    pts[3] = p;
                }
                if (Math.Abs(pts[0].X - pts[3].X) > 100)
                {
                    Point temp = pts[pts.Length - 1];
                    for (int i = pts.Length - 1; i >= 0; i--)
                    {
                        if (i == 0)
                            pts[i] = temp;
                        else
                            pts[i] = pts[i - 1];
                    }
                }
                //进行透视变换
                //1.确定变化尺寸的宽度

                float width1 = (pts[0].X - pts[1].X) * (pts[0].X - pts[1].X) + (pts[0].Y - pts[1].Y) * (pts[0].Y - pts[1].Y);
                float width2 = (pts[2].X - pts[3].X) * (pts[2].X - pts[3].X) + (pts[2].Y - pts[3].Y) * (pts[2].Y - pts[3].Y);
                width = width1 > width2 ? (int)Math.Sqrt(width1) : (int)Math.Sqrt(width2);
                //2.确定变化尺寸的高度

                float height1 = (pts[0].X - pts[3].X) * (pts[0].X - pts[3].X) + (pts[0].Y - pts[3].Y) * (pts[0].Y - pts[3].Y);
                float height2 = (pts[2].X - pts[1].X) * (pts[2].X - pts[1].X) + (pts[2].Y - pts[1].Y) * (pts[2].Y - pts[1].Y);
                height = height1 > height2 ? (int)Math.Sqrt(height1) : (int)Math.Sqrt(height2);
            }


            Point2f[] pts_src = Array.ConvertAll(pts.ToArray(), new Converter<Point, Point2f>(转换为浮点坐标));
            Point2f[] pts_target = new Point2f[] { new Point2f(0, 0), new Point2f(width - 1, 0), new Point2f(width - 1, height - 1), new Point2f(0, height - 1) };

            //4.计算透视变换矩阵
            //4.1类型转化
            Mat data = Cv2.GetPerspectiveTransform(pts_src, pts_target);
            //5.进行透视变换
            Mat birdMat = new Mat();
            //进行透视操作
            Cv2.WarpPerspective(mat, birdMat, data, new Size(width, height));
            return birdMat;
        }

        /// <summary>
        /// 筛选出选项轮廓。
        /// </summary>
        /// <param name="目标图">要是经过高斯模糊处理过的 Mat。</param>
        /// <param name="fillBlockHeight">填涂块高度。</param>
        /// <param name="fillBlockWidth">填涂块宽度。</param>
        /// <param name="outOffsetH">填涂块外溢偏移量高度（用于识别用户填涂到选项框外的情况）。</param>
        /// <param name="innerOffsetH">填涂块内收偏移量高度（用于识别用户填涂稍小的情况）。这个值一般应小于外溢偏移量。</param>
        /// <param name="outOffsetW">填涂块外溢偏移量宽度（用于识别用户填涂到选项框外的情况）。</param>
        /// <param name="innerOffsetW">填涂块内收偏移量宽度（用于识别用户填涂稍小的情况）。这个值一般应小于外溢偏移量。</param>
        /// <returns></returns>
        public static List<Point[]> 选取选项轮廓位置(Mat 目标图, double fillBlockHeight, double fillBlockWidth)
        {
            //轮廓筛选
            //1.改善轮廓
            Mat 结构元素图 = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(1, 1));
            Cv2.Dilate(目标图, 目标图, 结构元素图);
            //显示图像2(阈值图);
            //2.筛选轮廓
            Point[][] 目标轮廓双层数组;
            List<Point[]> 选中的轮廓的列表 = new List<Point[]>();
            HierarchyIndex[] hierarchly2;
            Cv2.FindContours(目标图, out 目标轮廓双层数组, out hierarchly2, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
            foreach (var m in 目标轮廓双层数组)
            {
                var rect = Cv2.BoundingRect(m);
                if (rect.Width > (fillBlockWidth * 0.7) && rect.Width < (fillBlockWidth * 2) &&
                    rect.Height > (fillBlockHeight * 0.9) && rect.Height < (fillBlockHeight * 2))
                {
                    选中的轮廓的列表.Add(m);
                }
            }
            return 选中的轮廓的列表;
        }


        /// <summary>
        /// 检测答题者的选项，获取涂选的结果数组
        /// </summary>
        /// <param name="阈值图">经阈值处理后的图像</param>
        /// <param name="classed_contours">经排序分类后的轮廓数组</param>
        /// <param name="countOfRow">一行中轮廓的个数</param>
        /// <param name="countOfColumn">一列中轮廓的个数</param>
        public static Mat 按答题卡定义取结果数组(Mat 阈值图, List<Point[]> 轮廓列表, 答题卡定义 ad,
                                                 ref List<string> 编号结果列表, ref List<string> 试题结果列表,
                                                 bool 精确匹配, bool 是否带批改信息)
        {
            double 单元格宽度 = (double)阈值图.Width / ad.答题卡列数.Value;
            double 单元格高度 = (double)阈值图.Height / ad.答题卡行数.Value;
            // 填涂块的高、宽是由所在单元格的高、宽决定的。

#if DEBUG
            // 绘制辅助线
            for (int i = 0; i < ad.答题卡行数; i++)
            {
                double top = 单元格高度 * (i + 1);
                阈值图.Line(new Point(0, top), new Point(阈值图.Width, top), Scalar.Blue);
            }

            for (int i = 0; i < ad.答题卡列数; i++)
            {
                double left = 单元格宽度 * (i + 1);
                阈值图.Line(new Point(left, 0), new Point(left, 阈值图.Height), Scalar.GreenYellow);
            }
#endif
            if (ad.编号条目列表 != null && ad.编号条目列表.Count > 0)
            {
                for (int n = 0; n < ad.编号条目列表.Count; n++)
                {
                    var 编号条目 = ad.编号条目列表[n];
                    var 编号选项文本 = "";
                    // int xxx = 0;
                    foreach (var 编号选项 in 编号条目.填涂选项集)
                    {
                        var left = (编号选项.坐标.列号 - 1) * 单元格宽度;
                        var top = (编号选项.坐标.行号 - 1) * 单元格高度;

                        var 数字矩形 = new OpenCvSharp.Rect((int)left, (int)top, (int)单元格宽度, (int)单元格高度);
                        // 阈值图.Rectangle(数字矩形, Scalar.Yellow);

                        if (精确匹配)
                        {
                            for (int i = 轮廓列表.Count - 1; i >= 0; i--)
                            {
                                var 轮廓 = 轮廓列表[i];
                                var 矩形 = Cv2.BoundingRect(轮廓);
                                var 填涂点数量 = 矩形.Width * 矩形.Height;
                                if (数字矩形.Contains(矩形))
                                {
                                    Mat 填涂块图 = new Mat(阈值图, 矩形);//提取ROI矩形区域

                                    using (var 灰度图 = new Mat())
                                    {
                                        Cv2.CvtColor(填涂块图, 灰度图, ColorConversionCodes.BGR2GRAY);
                                        int 非零像素数量 = Cv2.CountNonZero(灰度图);//计算图像内非零像素个数

                                        // 此轮廓就是对应选项
                                        if (非零像素数量 >= 填涂点数量 * ad.编号最小匹配比例)  // 填涂 3/4 以上的才算，防止数字或字母也被视为答案。
                                        {
                                            编号选项.是否被填涂 = true;
                                            编号选项文本 += 编号选项.选项文本;
                                            阈值图.Rectangle(矩形, Scalar.Yellow, 4);
                                        }
                                        else
                                        {
                                            编号选项.是否被填涂 = false;
                                        }
                                        轮廓列表.RemoveAt(i);   // 轮廓一旦被匹配，就不可能再匹配其它填涂项。
                                    }
                                }
                            }
                        }
                        else
                        {
                            // 仅根据单元格矩形内部小矩形来判断填充情况——不再考虑轮廓
                            // 这种不需要考虑轮廓，只看填充内容
                            // 小了  // var 填涂块矩形 = new OpenCvSharp.Rect(数字矩形.X + 数字矩形.Width / 4, 数字矩形.Y + 数字矩形.Height / 4, 数字矩形.Width / 2, 数字矩形.Height / 2);
                            // Mat 填涂块阈值图 = new Mat(阈值图, 填涂块矩形);//提取ROI矩形区域
                            Mat 填涂块阈值图 = new Mat(阈值图, 数字矩形);//提取ROI矩形区域
                            //阈值图.Rectangle(数字矩形, Scalar.DarkCyan);

                            Mat binaryImage = new Mat();

                            // 应用阈值操作进行二值化
                            double thresholdValue = 128; // 阈值可以根据需要调整
                            double maxValue = 255; // 最大值

                            // 使用全局阈值法 THRESH_BINARY 进行二值化
                            Cv2.Threshold(填涂块阈值图, binaryImage, thresholdValue, maxValue, ThresholdTypes.Binary);

                            using (var 灰度图 = new Mat())
                            {
                                Cv2.CvtColor(binaryImage, 灰度图, ColorConversionCodes.BGR2GRAY);
                                int 非零像素数量 = Cv2.CountNonZero(灰度图);//计算图像内非零像素个数

                                //显示图像(灰度图, xxx.ToString(), 1);
                                //xxx++;

                                #region 废弃按比例的说法——学生填的根本无法找到合适的比例
                                // 此轮廓就是对应选项
                                //if (非零像素数量 >= 填涂块矩形.Width * 填涂块矩形.Height * ad.编号最小匹配比例)
                                //{
                                //    编号选项.是否被填涂 = true;
                                //    编号选项文本 += 编号选项.选项文本;
                                //    阈值图.Rectangle(填涂块矩形, Scalar.Yellow, 4);
                                //}
                                //else
                                //{
                                //    编号选项.是否被填涂 = false;
                                //}
                                #endregion

                                编号选项.非零像素数量 = 非零像素数量;
                                //编号选项.填涂块矩形 = 填涂块矩形;
                                编号选项.填涂块矩形 = 数字矩形;
                            }
                        }
                    }

                    if (精确匹配 == false)
                    {
                        // 只考虑最大填涂项为选择项
                        var maxValue = 0;
                        for (int mindex = 0; mindex < 编号条目.填涂选项集.Count; mindex++)
                        {
                            var o = 编号条目.填涂选项集[mindex];
                            maxValue = Math.Max(maxValue, o.非零像素数量);
                        }

                        for (int mindex = 0; mindex < 编号条目.填涂选项集.Count; mindex++)
                        {
                            var 编号选项 = 编号条目.填涂选项集[mindex];
                            if (maxValue == 编号选项.非零像素数量 && maxValue >= 编号选项.填涂块矩形.Width * 编号选项.填涂块矩形.Height * ad.编号最小匹配比例 / 100)
                            {
                                编号选项.是否被填涂 = true;
                                编号选项文本 += 编号选项.选项文本;
                                阈值图.Rectangle(编号选项.填涂块矩形, Scalar.Yellow, 4);
                            }
                            else
                            {
                                编号选项.是否被填涂 = false;
                            }
                        }
                    }

                    if (string.IsNullOrEmpty(编号选项文本))
                        编号结果列表.Add("");   // 空值是要加的，用于判断是否识别错误。
                    else
                        编号结果列表.Add(编号选项文本);
                }
            }

            if (ad.试题条目列表 != null && ad.试题条目列表.Count > 0)
            {
                for (int q = 0; q < ad.试题条目列表.Count; q++)
                {
                    var 试题条目 = ad.试题条目列表[q];
                    var 试题选项文本 = "";
                    foreach (var 试题选项 in 试题条目.填涂选项集)
                    {
                        var left = (试题选项.坐标.列号 - 1) * 单元格宽度;
                        var top = (试题选项.坐标.行号 - 1) * 单元格高度;

                        var 试题矩形 = new OpenCvSharp.Rect((int)left, (int)top, (int)单元格宽度, (int)单元格高度);
                        //阈值图.Rectangle(试题矩形, Scalar.Green);

                        if (精确匹配)
                        {
                            for (int i = 轮廓列表.Count - 1; i >= 0; i--)
                            {
                                var 轮廓 = 轮廓列表[i];
                                var 矩形 = Cv2.BoundingRect(轮廓);
                                var 填涂像素数量 = 矩形.Width * 矩形.Height;

                                if (试题矩形.Contains(矩形))
                                {
                                    Mat 填涂块阈值图 = new Mat(阈值图, 矩形);//提取ROI矩形区域

                                    using (var 灰度图 = new Mat())
                                    {
                                        Cv2.CvtColor(填涂块阈值图, 灰度图, ColorConversionCodes.BGR2GRAY);
                                        int 非零像素数量 = Cv2.CountNonZero(灰度图);//计算图像内非零像素个数
                                                                           // 此轮廓就是对应选项
                                        if (非零像素数量 >= 填涂像素数量 * ad.试题最小匹配比例)
                                        {
                                            试题选项.是否被填涂 = true;
                                            string 批阅情况 = "";
                                            if (试题选项.是否被填涂.HasValue && 试题选项.是否被填涂.Value && 试题选项.是否应被填涂)
                                            {
                                                批阅情况 = 试题选项.选项文本 + (是否带批改信息 ? ",√" : "");
                                            }
                                            else
                                            {
                                                批阅情况 = 试题选项.选项文本 + (是否带批改信息 ? ",×" : "");
                                            }
                                            试题选项文本 += 批阅情况;
                                            阈值图.Rectangle(矩形, Scalar.Yellow, 4);
                                        }
                                        else
                                        {
                                            试题选项.是否被填涂 = false;
                                        }
                                        轮廓列表.RemoveAt(i);   // 轮廓一旦被匹配，就不可能再匹配其它填涂项。
                                    }
                                }
                            }
                        }
                        else
                        {
                            // 这种不需要考虑轮廓，只看填充内容
                            // 小了 // var 填涂块矩形 = new OpenCvSharp.Rect(试题矩形.X + 试题矩形.Width / 4, 试题矩形.Y + 试题矩形.Height / 4, 试题矩形.Width / 2, 试题矩形.Height / 2);
                            // Mat 填涂块阈值图 = new Mat(阈值图, 填涂块矩形);//提取ROI矩形区域
                            Mat 填涂块阈值图 = new Mat(阈值图, 试题矩形);//提取ROI矩形区域
                                                            // 创建一个目标矩阵用于存储二值化结果
                            Mat binaryImage = new Mat();

                            // 应用阈值操作进行二值化
                            double thresholdValue = 170; // 阈值可以根据需要调整
                            double maxValue = 255; // 最大值

                            // 使用全局阈值法 THRESH_BINARY 进行二值化
                            Cv2.Threshold(填涂块阈值图, binaryImage, thresholdValue, maxValue, ThresholdTypes.Binary);

                            using (var 灰度图 = new Mat())
                            {
                                Cv2.CvtColor(binaryImage, 灰度图, ColorConversionCodes.BGR2GRAY);
                                int 非零像素数量 = Cv2.CountNonZero(灰度图);//计算图像内非零像素个数

                                #region 废弃按比例的说法——学生填的根本无法找到合适的比例
                                // 此轮廓就是对应选项
                                //if (非零像素数量 >= 填涂块矩形.Width * 填涂块矩形.Height * ad.试题最小匹配比例)
                                //{
                                //    试题选项.是否被填涂 = true;
                                //    string 批阅情况 = "";
                                //    if (试题选项.是否被填涂.HasValue && 试题选项.是否被填涂.Value && 试题选项.是否应被填涂)
                                //    {
                                //        批阅情况 = 试题选项.选项文本 + (是否带批改信息 ? ",√" : "");
                                //    }
                                //    else
                                //    {
                                //        批阅情况 = 试题选项.选项文本 + (是否带批改信息 ? ",×" : "");
                                //    }
                                //    试题选项文本 += 批阅情况;
                                //    阈值图.Rectangle(填涂块矩形, Scalar.Yellow, 4);
                                //}
                                //else
                                //{
                                //    试题选项.是否被填涂 = false;
                                //}
                                #endregion

                                试题选项.非零像素数量 = 非零像素数量;
                                // 试题选项.填涂块矩形 = 填涂块矩形;
                                试题选项.填涂块矩形 = 试题矩形;
                            }
                        }
                    }

                    if (精确匹配 == false)
                    {
                        // 只考虑最大填涂项为选择项
                        var maxValue = 0;
                        for (int mindex = 0; mindex < 试题条目.填涂选项集.Count; mindex++)
                        {
                            var o = 试题条目.填涂选项集[mindex];
                            maxValue = Math.Max(maxValue, o.非零像素数量);
                        }

                        for (int mindex = 0; mindex < 试题条目.填涂选项集.Count; mindex++)
                        {
                            var 试题选项 = 试题条目.填涂选项集[mindex];
                            if (maxValue == 试题选项.非零像素数量 && maxValue >= 试题选项.填涂块矩形.Width * 试题选项.填涂块矩形.Height * ad.试题最小匹配比例 / 100)
                            {
                                试题选项.是否被填涂 = true;

                                string 批阅情况 = "";
                                if (试题选项.是否被填涂.HasValue && 试题选项.是否被填涂.Value && 试题选项.是否应被填涂)
                                {
                                    批阅情况 = 试题选项.选项文本 + (是否带批改信息 ? ",√" : "");
                                }
                                else
                                {
                                    批阅情况 = 试题选项.选项文本 + (是否带批改信息 ? ",×" : "");
                                }
                                试题选项文本 += 批阅情况;

                                阈值图.Rectangle(试题选项.填涂块矩形, Scalar.Yellow, 4);
                            }
                            else
                            {
                                试题选项.是否被填涂 = false;
                            }
                        }
                    }

                    if (string.IsNullOrEmpty(试题选项文本))
                        试题结果列表.Add("$");        // 空值也要加的，这样便于判断是否识读错误。
                    else
                        试题结果列表.Add(试题选项文本);
                }
            }

#if DEBUG
            显示图像(阈值图, "答案效果", 2);
#endif
            return 阈值图;
        }

        /// <summary>
        /// Point转换为PointF类型
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        public static Point2f 转换为浮点坐标(Point p)
        {
            return new Point2f(p.X, p.Y);
        }

    }
}
